package lol.clann.minecraft.springboot.core.condition;

import lol.clann.minecraft.springboot.SpringBootPlugin;
import lol.clann.minecraft.springboot.api.annotation.condition.ConditionalOnConfig;
import lol.clann.minecraft.springboot.api.annotation.condition.ConditionalOnConfigs;
import lol.clann.minecraft.springboot.api.context.PluginContext;
import lol.clann.minecraft.springboot.api.model.core.Plugin;
import relocate.ninja.leaping.configurate.commented.CommentedConfigurationNode;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

import java.util.*;

/**
 *
 *
 * {@link Condition} that checks if properties are defined in environment.
 *
 * @author Maciej Walkowiak
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @author Andy Wilkinson
 * @since 1.1.0
 * @see ConditionalOnConfig
 */
@Order(Ordered.HIGHEST_PRECEDENCE + 40)
public class OnConfigCondition extends SpringBootCondition {

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String className = getClassName(metadata);
        if (ConditionalOnConfigs.class.getName().equals(className)) {
            return ConditionOutcome.match();
        }
        Plugin plugin = getPlugin(className);
        if (plugin == null) {
            throw new IllegalStateException("can not find plugin of " + metadata.toString());
        }
        List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap(metadata.getAllAnnotationAttributes(ConditionalOnConfigs.class.getName()));
        List<ConditionMessage> noMatch = new ArrayList<>();
        List<ConditionMessage> match = new ArrayList<>();
        for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
            ConditionOutcome outcome = determineOutcome(plugin, annotationAttributes);
            (outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
        }
        if (!noMatch.isEmpty()) {
            return ConditionOutcome.noMatch(ConditionMessage.of(noMatch));
        }
        return ConditionOutcome.match(ConditionMessage.of(match));
    }

    private static Plugin getPlugin(String className) {
        Plugin plugin = PluginContext.getPluginByClass(className);
        if (plugin == null && className.startsWith("lol.clann.minecraft.springboot.")) {
            return PluginContext.getPlugin(SpringBootPlugin.id);
        } else {
            return null;
        }
    }

    private static String getClassName(AnnotatedTypeMetadata metadata) {
        String className;
        if (metadata instanceof ClassMetadata) {
            ClassMetadata classMetadata = (ClassMetadata) metadata;
            className = classMetadata.getClassName();
        } else {
            MethodMetadata methodMetadata = (MethodMetadata) metadata;
            className = methodMetadata.getDeclaringClassName();
        }
        return className;
    }

    private List<AnnotationAttributes> annotationAttributesFromMultiValueMap(
            MultiValueMap<String, Object> multiValueMap) {
        List<Map<String, Object>> maps = new ArrayList<>();
        multiValueMap.forEach((key, value) -> {
            for (int i = 0; i < value.size(); i++) {
                Map<String, Object> map;
                if (i < maps.size()) {
                    map = maps.get(i);
                } else {
                    map = new HashMap<>();
                    maps.add(map);
                }
                map.put(key, value.get(i));
            }
        });
        List<AnnotationAttributes> annotationAttributes = new ArrayList<>(maps.size());
        for (Map<String, Object> map : maps) {
            annotationAttributes.add(AnnotationAttributes.fromMap(map));
        }
        return annotationAttributes;
    }

    private ConditionOutcome determineOutcome(Plugin plugin, AnnotationAttributes annotationAttributes) {
        Specs specs = new Specs(annotationAttributes);
        List<String> missingProperties = new ArrayList<>();
        List<String> nonMatchingProperties = new ArrayList<>();
        for (Spec spec : specs.specs) {
            spec.collectProperties(plugin, missingProperties, nonMatchingProperties);
        }

        if (!missingProperties.isEmpty()) {
            return ConditionOutcome.noMatch(
                    ConditionMessage.forCondition(ConditionalOnConfigs.class, specs)
                            .didNotFind("property", "properties")
                            .items(Style.QUOTE, missingProperties));
        }
        if (!nonMatchingProperties.isEmpty()) {
            return ConditionOutcome.noMatch(
                    ConditionMessage.forCondition(ConditionalOnConfigs.class, specs)
                            .found("different value in property",
                                    "different value in properties")
                            .items(Style.QUOTE, nonMatchingProperties));
        }
        return ConditionOutcome.match(ConditionMessage
                .forCondition(ConditionalOnConfigs.class, specs).because("matched"));
    }


    private static class Specs {
        Spec[] specs;

        Specs(AnnotationAttributes annotationAttributes) {
            AnnotationAttributes[] configs = annotationAttributes.getAnnotationArray("value");
            specs = new Spec[configs.length];
            for (int i = 0; i < configs.length; i++) {
                specs[i] = new Spec(configs[i]);
            }
        }

        @Override
        public String toString() {
            final StringBuilder sb = new StringBuilder("Specs{");
            sb.append("specs=").append(Arrays.toString(specs));
            sb.append('}');
            return sb.toString();
        }
    }

    private static class Spec {

        private final String prefix;

        private final String[] havingValue;

        private final String[] names;

        private final boolean matchIfMissing;

        Spec(AnnotationAttributes annotationAttributes) {
            String prefix = annotationAttributes.getString("prefix").trim();
            if (StringUtils.hasText(prefix) && !prefix.endsWith(".")) {
                prefix = prefix + ".";
            }
            this.prefix = prefix;
            this.havingValue = annotationAttributes.getStringArray("havingValue");
            this.names = annotationAttributes.getStringArray("value");
            this.matchIfMissing = annotationAttributes.getBoolean("matchIfMissing");
        }

        private void collectProperties(Plugin plugin, List<String> missing,
                                       List<String> nonMatching) {
            for (String name : this.names) {
                String key = this.prefix + name;
                CommentedConfigurationNode node = plugin.getConfigurationNode().getNode(key.split("\\."));
                if (node.isVirtual()) {
                    if (!matchIfMissing) {
                        missing.add(key);
                    }
                } else {
                    String v = node.getString();
                    boolean match = false;
                    for (String s : havingValue) {
                        if (Objects.equals(v, s)) {
                            match = true;
                            break;
                        }
                    }
                    if (!match) {
                        nonMatching.add(key);
                    }
                }
            }
        }

        @Override
        public String toString() {
            StringBuilder result = new StringBuilder();
            result.append("(");
            result.append(this.prefix);
            if (this.names.length == 1) {
                result.append(this.names[0]);
            } else {
                result.append("[");
                result.append(StringUtils.arrayToCommaDelimitedString(this.names));
                result.append("]");
            }
            if (this.havingValue.length > 0) {
                result.append("=").append(Arrays.toString(this.havingValue));
            }
            result.append(")");
            return result.toString();
        }

    }

}
